import * as path from "node:path";
import * as fs from "node:fs";
import configuration from "src/common/configuration";
import { Injectable } from "@nestjs/common";
import { PrismaClient } from "@prisma/client";
import { MIMEType } from "node:util";
import mime from "mime";
import { MimeType } from "mime-type";
import mimeDB from "mime-db";
const lookupMimeType = (function(){
  const mimeType = new MimeType(mimeDB);
  return mimeType.lookup.bind(mimeType);
})();

const RESOURCE_DIRECTORY_PATH = configuration.resource.rootDirectoryPath;

export type ResourceType =
  | "text"
  | "audio"
  | "image"
  | "video"
  | "font"
  | "model"
  | "document"
  | "compressed"
  | "other";
const RESOURCE_TYPE_VALUES = [
  "text",
  "audio",
  "image",
  "video",
  "font",
  "model",
  "document",
  "compressed",
  "other",
];
for (const t of RESOURCE_TYPE_VALUES) {
  fs.mkdirSync(path.join(RESOURCE_DIRECTORY_PATH, t), { recursive: true });
}

export interface ResourceInformation {
  uploaderID: number;
  path: string;
  name: string;
  description?: string;
  mimeType: string;
  size: number;
  resourceType: ResourceType,
  public?: boolean;
}

@Injectable()
export class ResourceService {
  // 100KB - 1000MS
  // 1000MS = 102400B
  // 1ms = 102.4B
  static PS_UPLOAD_SPEED: number = 1024 * 50; // 单位为 字节，100KB
  static CHUNK_SIZE: number = 1024 * 1024 * 100; // 单位为 字节，100MB
  static CHUNK_CACHE_TIME: number =
    ResourceService.CHUNK_SIZE / (ResourceService.PS_UPLOAD_SPEED / 1000); // ms
  codeLastUploadTimeMap: Map<string, number> = new Map();
  codeStreamMap: Map<string, fs.WriteStream> = new Map();
  codeInfoMap: Map<string, ResourceInformation> = new Map();

  constructor(private readonly prisma: PrismaClient) {
    setInterval(() => {
      for (const [
        code,
        lastUploadTime,
      ] of this.codeLastUploadTimeMap.entries()) {
        if (Date.now() - lastUploadTime > ResourceService.CHUNK_CACHE_TIME) {
          this.removeUploadCache(code, true);
        }
      }
    }, ResourceService.CHUNK_CACHE_TIME);
  }

  async getResourceByID(id: number) {
    return await this.prisma.resource.findUnique({
      include: {
        user: {
          select: {
            nickname: true,
          }
        },
      },
      where: {
        id,
      },
    });
  }

  async getResourceByPage(page: number, pageSize: number = 10) {
    return await this.prisma.resource.findMany({
      include: {
        user: {
          select: {
            nickname: true,
          }
        },
      },
      orderBy: {
        upload_time: "desc",
      },
      skip: (page - 1) * pageSize,
      take: pageSize,
    });
  }

  getMIMETypeByFileName(fileName: string) {
    if (mime) {
      return mime.getType(fileName);
    } else {
      const result = lookupMimeType(fileName);
      if (Array.isArray(result)) {
        return result[0];
      }
      return result;
    }
  }

  isResourceType(type: string): type is ResourceType {
    return RESOURCE_TYPE_VALUES.includes(type);
  }

  getResourceTypeByMIMEType(type: string): ResourceType {
    const mimeType = new MIMEType(type);
    if (
      ["text", "audio", "image", "video", "font", "model"].includes(
        mimeType.type,
      )
    ) {
      return mimeType.type as ResourceType;
    }
    if (
      [
        // doc(x)
        "x-abiword",
        "msword",
        "vnd.msword",
        "x-msword",
        "vnd.openxmlformats-officedocument.wordprocessingml.document",
        // xls(x)
        "msexcel",
        "x-msexcel",
        "vnd.msexcel",
        "vnd.openxmlformats-officedocument.spreadsheetml.sheet",
        // ppt(x)
        "vnd.ms-powerpoint",
        "vnd.openxmlformats-officedocument.presentationml.presentation",
        // pdf
        "pdf",
        "x-pdf",
        "acrobat",
        "nappdf",
        // other
        "vnd.oasis.opendocument.presentation",
        "vnd.oasis.opendocument.spreadsheet",
        "vnd.oasis.opendocument.text",
      ].includes(mimeType.subtype)
    ) {
      return mimeType.subtype as ResourceType;
    }
    if (
      [
        "x-compressed",
        "zip",
        "x-zip",
        "x-tar",
        "x-rar",
        "vnd.rar",
        "x-rar-compressed",
        "gzip",
        "x-gzip",
        "x-bzip",
        "x-bzip2",
        "x-7z-compressed",
        "java-archive",
      ].includes(mimeType.subtype)
    ) {
      return "compressed";
    }
    return "other";
  }

  getResourceTypeByFileName(fileName: string): ResourceType {
    const mimeType = this.getMIMETypeByFileName(fileName);
    return this.getResourceTypeByMIMEType(mimeType);
  }

  getResourceTypeByExtension(ext: string): ResourceType {
    const fileName = ext.startsWith(".") ? "example" + ext : "example." + ext;
    return this.getResourceTypeByFileName(fileName);
  }

  getResourceDirectoryPathByType(type: ResourceType) {
    return path.join(RESOURCE_DIRECTORY_PATH, type);
  }

  getResourceSavePath(
    uploaderID: number,
    fileName: string,
    resourceType?: ResourceType,
  ) {
    if (!resourceType) {
      resourceType = this.getResourceTypeByFileName(fileName);
    }
    const directoryPath = this.getResourceDirectoryPathByType(resourceType);
    const userDirectoryPath = path.join(directoryPath, uploaderID.toString());
    if (!fs.existsSync(userDirectoryPath)) {
      fs.mkdirSync(userDirectoryPath, { recursive: true });
    }
    return path.join(userDirectoryPath, fileName);
  }

  /**
   * @param code code
   * @param removeFile 是否删除文件
   * @returns cache size in bytes
   */
  removeUploadCache(
    code: string,
    removeFile: boolean = false,
  ): Promise<number> {
    return new Promise((resolve) => {
      const ws = this.codeStreamMap.get(code);
      const info = this.codeInfoMap.get(code);
      const filePath = this.getResourceSavePath(info.uploaderID, info.name);
      this.codeLastUploadTimeMap.delete(code);
      this.codeStreamMap.delete(code);
      this.codeInfoMap.delete(code);

      const getFileSize = () => {
        if (!fs.existsSync(filePath)) {
          return 0;
        }
        const stat = fs.statSync(filePath);
        return stat.size;
      };

      if (ws) {
        ws.end(() => {
          const cacheSize = getFileSize();
          if (removeFile) {
            fs.rmSync(filePath);
          }
          resolve(cacheSize);
        });
      } else {
        const cacheSize = getFileSize();
        if (removeFile) {
          fs.rmSync(filePath);
        }
        resolve(cacheSize);
      }
    });
  }

  startUploadChunk(info: ResourceInformation): string {
    const filePath = this.getResourceSavePath(info.uploaderID, info.name);
    const writeStream = fs.createWriteStream(filePath);
    const code = Math.random().toString(36).slice(2);
    this.codeStreamMap.set(code, writeStream);
    this.codeInfoMap.set(code, info);
    return code;
  }

  async endUploadChunk(code: string): Promise<ResourceInformation | null> {
    const info = this.codeInfoMap.get(code);
    info.size = await this.removeUploadCache(code);
    return info;
  }

  saveChunk(code: string, chunk: Buffer): Promise<boolean> {
    return new Promise((resolve) => {
      const ws = this.codeStreamMap.get(code);
      if (!ws) {
        resolve(false);
      }
      ws.write(chunk, (err) => {
        if (err) {
          console.error(err);
          resolve(false);
          return;
        }
        resolve(true);
      });
    });
  }
}
